﻿#region License and Terms
// MoreLINQ - Extensions to LINQ to Objects
// Copyright (c) 2008 Jonathan Skeet. All rights reserved.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// These methods © 2013 Jon Hanna. Contributed to the above project.
// Licenses under the Apache License, Version 2.0 as per above.
#endregion
namespace MoreLinq
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
#if !NO_EXCEPTION_SERIALIZATION
    using System.Runtime.Serialization;
#endif

    /// <summary>
    /// A call to <see cref="MoreEnumerable.TakeAtLeast{T}"/> failed because there was not a sufficent
    /// number of elements in the sequence.
    /// </summary>
    /// <remarks>
    /// This class inherits from <see cref="InvalidOperationException"/> as that is thrown in the comparable
    /// case of <see cref="Enumerable.First{TSource}(IEnumerable{TSource})"/> finding no elements, and so on. Meanwhile, being separately
    /// defined allows it to be caught separately, if required.
    /// </remarks>
#if !NO_EXCEPTION_SERIALIZATION
    [Serializable]
#endif
    public class AtLeastUnfulfilledException : InvalidOperationException
    {
        internal AtLeastUnfulfilledException()
            :base("There was not a sufficient number of items in the sequence")
        {
        }
#if !NO_EXCEPTION_SERIALIZATION
        private AtLeastUnfulfilledException(SerializationInfo info, StreamingContext context)
            :base(info, context)
        {
        }
#endif
    }
    public static partial class MoreEnumerable
    {
        /// <summary>
        /// Ensures that there are at least a certain number of elements in the sequence.
        /// If necessary, up to that number of elements will be loaded into memory immediately,
        /// but subsequence elements will be created in a lazy manner.
        /// </summary>
        /// <param name="source">The source sequence.</param>
        /// <param name="required">The minimum number of items required.</param>
        /// <returns></returns>
        public static IEnumerable<T> TakeAtLeast<T>(this IEnumerable<T> source, int required)
        {
        	if(source == null) throw new ArgumentNullException("source");
        	if(required < 0) throw new ArgumentOutOfRangeException("required", required, "Required count must be greater than or equal to zero.");
        	if(required == 0)
        	    return source;
        	ICollection<T> collection = source as ICollection<T>;
        	if(collection != null) //Calling .Count will almost certainly be faster; generally O(1) rather than O(n) set-up
        	                       //This also avoids duplicate in-memory stores.
        	{
        	    if(collection.Count < required)
        	        return new UnfulfilledAtLeastEnumerable<T>();
        		return source;
        	}
        	return new AtLeastEnumerable<T>(source, required);
        }
        private class UnfulfilledAtLeastEnumerable<T> : IEnumerable<T>
        {
            public IEnumerator<T> GetEnumerator()
            {
                throw new AtLeastUnfulfilledException();
            }
            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
        private class AtLeastEnumerable<T> : IEnumerable<T>
        {
            private readonly IEnumerable<T> _source;
            private readonly int _atLeast;
            public AtLeastEnumerable(IEnumerable<T> source, int atLeast)
            {
                _source = source;
                _atLeast = atLeast;
            }
            public IEnumerator<T> GetEnumerator()
            {
                return new AtLeastEnumerator<T>(_source, _atLeast);
            }
            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
        private class AtLeastEnumerator<T> : IEnumerator<T>
        {
            private readonly IEnumerator<T> _sourceEn;
            private List<T>.Enumerator _storeEn;
            private bool _fromStore;
            public AtLeastEnumerator(IEnumerable<T> source, int atLeast)
            {
                _sourceEn = source.GetEnumerator();
                var store = new List<T>(atLeast);
                while(atLeast != 0 && _sourceEn.MoveNext())
                {
                    store.Add(_sourceEn.Current);
                    atLeast--;
                }
                if(atLeast != 0)
                {
                    _sourceEn.Dispose();
                    throw new AtLeastUnfulfilledException();
                }
                _storeEn = store.GetEnumerator();
                _fromStore = true;
            }            
            public T Current
            {
                get { return _fromStore ? _storeEn.Current : _sourceEn.Current; }
            }
            object IEnumerator.Current
            {
                get { return Current; }
            }
            public void Dispose()
            {
                _sourceEn.Dispose();
            }
            public bool MoveNext()
            {
                if(_fromStore)
                {
                    if(_storeEn.MoveNext())
                        return true;
                    _fromStore = false;
                }
                return _sourceEn.MoveNext();
            }
            void IEnumerator.Reset()
            {
                throw new NotSupportedException();
            }
        }
    }
}
